package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * <ul>
 * <li>Author: 		chs</li>
 * <li>E-Mail:		935369680@qq.com</li>
 * <li>Date: 		 @Date 2024/3/6 16:44 </li>
 * <li>Description: 缓存工具类	 </li>
 * <li>+-History-------------------------------------+</li>getDictionary
 * <li>| Date		     Author		Description	</li>
 * <li>| @Date 2024/3/6 16:44         chs       Created   </li>
 * <li>+------------------------------------------------</li>
 * </ul>
 *
 * @author chs
 */
@Slf4j
@Component
public class CacheClient {
    //    redis
    private final StringRedisTemplate stringRedisTemplate;
    //    线程池
    //    创建一个有十个线程的线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 保存数据到内存中，并设置过期时间
     *
     * @param key   key
     * @param value 值
     * @param time  过期时间
     * @param unit  过期时间单位
     * @Title: set
     * @Description
     * @Author chs
     * @Date 2024/3/6 16:47
     * @Return void 无返回值
     * @Throws
     **/
    public void set(String key, Object value, Long time, TimeUnit unit) {

        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);

    }

    /**
     * 保存数据到内存中，并设逻辑过期时间
     *
     * @param key   key
     * @param value 值
     * @param time  过期时间
     * @param unit  过期时间单位
     * @Title: set
     * @Description
     * @Author chs
     * @Date 2024/3/6 16:47
     * @Return void 无返回值
     * @Throws
     **/
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
//设置逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(value);
//        unit.toSeconds(time)) 将任意时间单位改为秒的时间单位
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 设置空值，解决缓存穿透问题
     *
     * @param type       返回值类型
     * @param keyPrefix  key前缀
     * @param id         查询参数id
     * @param dbFallback 执行sql查询的方法
     * @param time       过期时间
     * @param unit       时间单位
     * @Title: queryWithPassThrough
     * @Description
     * @Author chs
     * @Date 2024/3/8 9:49
     * @Return R 返回值
     * @Throws
     **/

    public <R, ID> R queryWithPassThrough(Class<R> type, String keyPrefix, ID id, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
//    key前缀加上id组合成唯一key
        String key = keyPrefix + id;
//        在缓存中查询数据
        String json = stringRedisTemplate.opsForValue().get(key);
//        命中返回结果
        if (StrUtil.isNotBlank(json)) {
            return BeanUtil.toBean(json, type);
        }
//        判断命中的是否为空值
        if (null == json) {
            return null;
        }
        R r = dbFallback.apply(id);
        if (r == null) {
            stringRedisTemplate.opsForValue().set(key, "", time, unit);
            return null;
        }
        this.set(key, JSONUtil.toJsonStr(r), time, unit);
        return r;
    }

    /**
     * 逻辑过期 解决缓存击穿问题
     * @param type 返回值类型
     * @param id 主键
     * @param keyPrefix 查询key前缀
     * @param dbFallback 查询数据库的回调函数
     * @param time 超时时间
     * @param unit 时间点位
     * @return
     * @param <R> 返回值类型
     * @param <ID> 主键类型
     */
    public <R, ID> R queryWithLogicalExpire(Class<R> type, ID id, String keyPrefix, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(json)) {
            return null;
        }
        RedisData data = JSONUtil.toBean(json, RedisData.class);
        R r1 = BeanUtil.toBean(data.getData(), type);
//      判断是否过期
        if (LocalDateTime.now().isAfter(data.getExpireTime())) {
//          未过期返回数据
            return r1;
        }
//        已过期重建缓存
//        1.1尝试获取锁
        String lockLey = RedisConstants.LOCK_SHOP_KEY + id;
        try {
            Boolean isLock = this.tryLock(lockLey);
            if (isLock) {
//        1.2开启线程查询数据库
                CACHE_REBUILD_EXECUTOR.submit(() -> {
//                 1.2.1查询数据库
                    R r = dbFallback.apply(id);
//                 1.2.2写入缓存
                    this.setWithLogicalExpire(key, JSONUtil.toJsonStr(r), time, unit);
                    return r;
                });
            }
//           没有获取到锁的任然返回旧值。
            return r1;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            this.unLock(lockLey);
        }

    }

    /**
     * 尝试获取锁
     * 过期时间2秒钟
     *
     * @param key 锁的key
     * @Title: tryLock
     * @Description
     * @Author chs
     * @Return Boolean 获取成功返回 true
     * @Throws
     **/
    public Boolean tryLock(String key) {
        return stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 2, TimeUnit.SECONDS);
    }

    /**
     * 释放互斥锁
     *
     * @param key
     * @Title: unLock
     * @Description
     * @Author chs
     * @Return void
     * @Throws
     **/
    public void unLock(String key) {
        stringRedisTemplate.delete(key);

    }

}
